/*
 * Copyright (C) 2008-2011  Paulo Cesar Pereira de Andrade.
 *
 * This is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * Authors:
 *	Paulo Cesar Pereira de Andrade
 */

#include "exl.h"

/*
 * Prototypes
 */
static etag_t *
make_tag_record(erecord_t *record, etagtype_t type);

static etag_t *
make_tag_vector(etag_t *base, eword_t length);

static etag_t *
make_tag_function(etag_t *base, evector_t *vector);

static etag_t *
tag_ast_data_vector(etag_t *base, east_t *ast);

static etag_t *
tag_vector_check(etag_t *base, eword_t length, east_t *ast);

/*
 * Initialization
 */
etag_t			*auto_tag;
etag_t			*string_tag;

static ehash_t		*tag_table;
static ehash_t		*alias_table;
static evector_t	*key_vector;
static evector_t	*proto_vector;
static evector_t	*convert_vector;
static struct {
    char		*name;
    eint32_t		 size;
    etype_t		 type;
} types[] = {
    { "void",		0,			t_void    },
#if __WORDSIZE == 32
    { "int32_t",	4,			t_int32   },
#else
    { "int64_t",	8,			t_int64   },
#endif
    { "int8_t",		1,			t_int8    },
    { "uint8_t",	1,			t_uint8   },
    { "int16_t",	2,			t_int16   },
    { "uint16_t",	2,			t_uint16  },
#if __WORDSIZE == 64
    { "int32_t",	4,			t_int32   },
#endif
    { "uint32_t",	4,			t_uint32  },
#if __WORDSIZE == 32
    { "int64_t",	8,			t_int64	  },
#endif
    { "uint64_t",	8,			t_uint64  },
    { "float32_t",	4,			t_float32 },
    { "float64_t",	8,			t_float64 },
    /* internal, used to fill type information */
    { "*rat_t*",	sizeof(eobject_t),	t_rat	  },
    { "*mpz_t*",	sizeof(eobject_t),	t_mpz	  },
    { "*mpq_t*",	sizeof(eobject_t),	t_mpq	  },
    { "*mpr_t*",	sizeof(eobject_t),	t_mpr	  },
    { "*cdd_t*",	sizeof(eobject_t),	t_cdd	  },
    { "*cqq_t*",	sizeof(eobject_t),	t_cqq	  },
    { "*mpc_t*",	sizeof(eobject_t),	t_mpc	  },
};

/*
 * Implementation
 */
void
init_tag(void)
{
    eint32_t		 offset;
    esymbol_t		*symbol;

    eadd_root((eobject_t *)&tag_table);
    enew_hash((eobject_t *)&tag_table, 128);

    /* cache of alias conversion information */
    eadd_root((eobject_t *)&alias_table);
    enew_hash((eobject_t *)&alias_table, 8);

    /* force check by contents */
    eadd_root((eobject_t *)&key_vector);
    enew_vector((eobject_t *)&key_vector, t_uint8, sizeof(eword_t));

    /* vector of vectors to check tags */
    eadd_root((eobject_t *)&proto_vector);
    enew_vector((eobject_t *)&proto_vector, t_void, 16);

    /* vector to store information to canonicalize vector tags */
    eadd_root((eobject_t *)&convert_vector);
    enew_vector((eobject_t *)&convert_vector, t_void, 4);

    for (offset = 0; offset < esize(types); offset++) {
	symbol = enew_identifier(eget_string((euint8_t *)types[offset].name,
					     strlen(types[offset].name)));
	enew_basic(symbol, types[offset].type, types[offset].size);
	symbol->type = true;
    }

    string_tag = etag_vector(etag_object(type_vector->v.ptr[t_uint8]), 0);

    eadd_root((eobject_t *)&auto_tag);
    enew((eobject_t *)&auto_tag, tag);
    auto_tag->type = tag_auto;
    auto_tag->size = sizeof(eobject_t);

    symbol = enew_identifier(eget_string((euint8_t *)"auto", 4));
    symbol->type = true;
    symbol->tag = auto_tag;
}

void
finish_tag(void)
{
    erem_root((eobject_t *)&tag_table);
    erem_root((eobject_t *)&alias_table);
    erem_root((eobject_t *)&key_vector);
    erem_root((eobject_t *)&proto_vector);
    erem_root((eobject_t *)&convert_vector);
    erem_root((eobject_t *)&auto_tag);
}

etag_t *
etag_object(eobject_t object)
{
    etag_t		*tag;

    if (object == null)
	return (null);

    switch (etype(object)) {
	case t_basic:
	    if ((tag = (etag_t *)eget_hash(tag_table, object)) == null)
		tag = make_tag_record(object, tag_basic);
	    break;
	case t_class:
	    if ((tag = (etag_t *)eget_hash(tag_table, object)) == null)
		tag = make_tag_record(object, tag_class);
	    break;
	case t_tag:
	    tag = object;
	    break;
	default:
	    tag = auto_tag;
	    break;
    }

    return (tag);
}

etag_t *
etag_vector(etag_t *base, eword_t length)
{
    etag_t		*tag;

#if DEBUG
    /* caller must check for file/line/column context */
    assert(length >= 0);
#endif

    if (base->data == null) {
	enew_hash((eobject_t *)&base->data, 8);
	tag = make_tag_vector(base, length);
    }
    else {
	*key_vector->v.w = length;
	assert(ehash_p(base->data));
	tag = (etag_t *)eget_hash((ehash_t *)base->data, key_vector);
	if (tag == null)
	    tag = make_tag_vector(base, length);
    }

    return (tag);
}

etag_t *
etag_vector_alias(etag_t *tag)
{
    etag_t		*base;
    etag_t		*temp;
    eentry_t		*entry;
    eword_t		 offset;
    evector_t		*vector;
    eobject_t		*pointer;

    for (base = tag, offset = 0; base->base; base = base->base, offset++)
	;
    /*   Only canonicalize access to vectors that may alias. */
    if (base->type == tag_auto || base->type == tag_basic)
	return (tag);

    if ((entry = eget_hash(alias_table, tag)))
	return (entry->value);

    if (offset >= convert_vector->length)
	erenew_vector(convert_vector, (offset + 8) & ~7);
    convert_vector->offset = offset;
    for (base = tag, offset--; offset >= 0; base = base->base, offset--)
	convert_vector->v.ptr[offset] = base;

    offset = 0;
    base = auto_tag;
    for (; offset < convert_vector->offset; offset++, base = tag) {
	temp = convert_vector->v.ptr[offset];
	vector = temp->name;
	if (base->data == null) {
	    enew_hash((eobject_t *)&base->data, 8);
	    tag = make_tag_vector(base, *vector->v.w);
	}
	else {
	    *key_vector->v.w = *vector->v.w;
	    assert(ehash_p(base->data));
	    tag = (etag_t *)eget_hash((ehash_t *)base->data, key_vector);
	    if (tag == null)
		tag = make_tag_vector(base, *vector->v.w);
	}
	gc_ref(pointer);
	enew_entry(pointer, temp, tag);
	eput_hash(alias_table, *pointer);
	gc_dec();
    }

    return (tag);
}


etag_t *
etag_prototype(etag_t *base, east_t *decl)
{
    east_t		*ast;
    etag_t		*tag;
    east_t		*list;
    east_t		*type;
    eint32_t		 length;
    eint32_t		 offset;
    evector_t		*vector;

    /* remove void as single argument */
    if ((ast = decl->r.ast) && ast->token == tok_type) {
	tag = ast->l.value;
	if (tag->type == tag_basic && tag->size == 0) {
	    if (ast->next)
		eparse_error(ast, "'void' must be single argument");
	    edel_object(&decl->r.value);
	}
    }
    /* add type tag for auto */
    else {
	east_t		*temp;
	eobject_t	*pointer;

	for (; ast; ast = ast->next) {
	    if (ast->token == tok_symbol || ast->token == tok_vector) {
		gc_ref(pointer);
		enew_ast(pointer, ast->token, ast->note.name,
			 ast->note.lineno, ast->note.column);
		temp = *pointer;
		temp->l.value = ast->l.value;
		temp->r.value = ast->r.value;
		gc_pop(ast->r.value);
		gc_ref(pointer);
		enew_ast(pointer, tok_type, ast->note.name,
			 ast->note.lineno, ast->note.column);
		temp = *pointer;
		temp->l.value = auto_tag;
		gc_pop(ast->l.value);
		ast->token = tok_declexpr;
	    }
	}
    }

    for (list = decl->r.ast, length = 1; list; list = list->next, length++)
	;

    /* avoid allocating an object for checking */
    if (length >= proto_vector->length)
	erenew_vector(proto_vector, (length + 15) & ~15);
    if ((vector = proto_vector->v.ptr[length]) == null) {
	enew_vector(proto_vector->v.ptr + length,
		    t_uint8, length * sizeof(eobject_t));
	vector = proto_vector->v.ptr[length];
    }

    vector->v.ptr[0] = base;
    for (list = decl->r.ast, offset = 1; list; list = list->next, offset++) {
	if (list->token == tok_type) {
	    type = list;
	    ast = null;
	}
	else {
	    if (list->token != tok_declexpr)
		eparse_error(list, "expecting declaration");
	    type = list->l.ast;
	    ast = list->r.ast;
	    assert(type->token == tok_type);
	}
	vector->v.ptr[offset] = etag_ast(type->l.value, &ast);
    }

    if (base->data == null) {
	enew_hash((eobject_t *)&base->data, 8);
	tag = make_tag_function(base, vector);
    }
    else {
	assert(ehash_p(base->data));
	tag = (etag_t *)eget_hash((ehash_t *)base->data, vector);
	if (tag == null)
	    tag = make_tag_function(base, vector);
    }

    return (tag);
}

etag_t *
etag_ast(etag_t *tag, east_t **list)
{
    east_t		*ast;
    eword_t		 length;

    if ((ast = *list) == null)
	return (tag);
    for (;;) {
	switch (ast->token) {
	    case tok_vector:
		if (ast->r.ast->token != tok_number ||
		    etype(ast->r.ast->l.value) != t_word)
		    eparse_error(ast->r.ast->l.ast, "not an integer");
		length = *(eword_t *)ast->r.ast->l.value;
		if (length < 0)
		    eparse_error(ast->r.ast->l.ast, "negative length");
		tag = tag_vector_check(tag, length, ast->r.ast);
		if ((ast = ast->l.value) == null) {
		    *list = null;
		    return (tag);
		}
		break;
	    case tok_call:
	    case tok_symbol:
		*list = ast;
		return (tag);
	    default:
		eparse_error(ast, "expecting expression");
	}
    }
}

etag_t *
etag_ast_data(etag_t *tag, east_t *ast)
{
    evector_t		*vector;

    if (ast->token == tok_data)
	return (tag_ast_data_vector(tag, ast->l.ast));
    else if (ast->token == tok_string) {
	vector = ast->l.value;
	return (tag_vector_check(tag->base, vector->length, ast));
    }

    return (tag);
}

static etag_t *
make_tag_record(erecord_t *record, etagtype_t type)
{
    etag_t		*tag;
    eobject_t		*pointer;

    if ((tag = (etag_t *)eget_hash(tag_table, record)) == null) {
	gc_ref(pointer);
	enew(pointer, tag);
	tag = *pointer;
	tag->name = record;
	tag->type = type;
	if (record->type <= t_float32)
	    tag->size = record->length;
	else
	    tag->size = sizeof(eobject_t);
	eput_hash(tag_table, (eentry_t *)tag);
	gc_dec();
    }

    return (tag);
}

static etag_t *
make_tag_vector(etag_t *base, eword_t length)
{
    etag_t		*tag;
    eword_t		 size;
    evector_t		*vector;
    eobject_t		*pointer;

    if (length == 0)
	size = 0;
    else if ((size = base->size * length) < base->size)
	return (null);

    gc_ref(pointer);
    enew(pointer, tag);
    tag = *pointer;
    enew_vector(&tag->name, t_uint8, sizeof(eword_t));
    vector = tag->name;
    *vector->v.w = length;
    tag->type = tag_vector;
    tag->size = size;
    tag->base = base;
    assert(ehash_p(base->data));
    eput_hash((ehash_t *)base->data, (eentry_t *)tag);
    gc_dec();

    return (tag);
}

static etag_t *
make_tag_function(etag_t *base, evector_t *vector)
{
    etag_t		*tag;
    eobject_t		*pointer;

    gc_ref(pointer);
    enew(pointer, tag);
    tag = *pointer;
    enew_vector(&tag->name, t_uint8, vector->length);
    memcpy(((evector_t *)tag->name)->v.ptr, vector->v.ptr, vector->length);
    tag->type = tag_function;
    tag->size = sizeof(eobject_t);
    tag->base = base;
    eput_hash((ehash_t *)base->data, (eentry_t *)tag);
    gc_dec();

    return (tag);
}

static etag_t *
tag_ast_data_vector(etag_t *tag, east_t *ast)
{
    east_t		*rast;
    etag_t		*btag;
    etag_t		*rtag;
    eword_t		 length;
    eword_t		 offset;

    btag = tag->base;
    for (length = 0; ast; ast = ast->next, length++) {
	if (ast->token == tok_set) {
	    rast = ast->l.value;
	    if (rast->token != tok_elemref)
		eparse_error(ast, "unexpected '='");
	    rast = rast->l.value;
	    if (rast->token != tok_number || etype(rast->l.value) != t_word)
		eparse_error(rast, "not an integer");
	    offset = *(eword_t *)rast->l.value;
	    if (offset < 0)
		eparse_error(rast, "negative offset");
	    if (length < offset)
		length = offset;
	    rtag = etag_ast_data(btag, ast->r.value);
	}
	else
	    rtag = etag_ast_data(btag, ast);
	if (rtag->size > btag->size)
	    btag = rtag;
    }
    if (btag->size * length > tag->size)
	return (tag_vector_check(btag, length, ast));

    return (tag);
}

static etag_t *
tag_vector_check(etag_t *base, eword_t length, east_t *ast)
{
    etag_t		*tag;

    if ((tag = etag_vector(base, length)) == null)
	eparse_error(ast, "offset too large");

    return (tag);
}
